//----------------------------------------------------------------------------
/** @file Engine.cpp */
//----------------------------------------------------------------------------
#include "stdafx.h"

#include "Engine.h"

#include <cassert>
#include <iomanip>
#include <iostream>
#include <sstream>
#include "Interface.h"
#include "Platform.h"

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

//----------------------------------------------------------------------------

namespace amigogtp {

using namespace std;

//----------------------------------------------------------------------------

namespace
{
    Color BlackWhiteArg(GtpCommand& cmd, size_t number)
    {
        string arg = cmd.ArgToLower(number);
        if (arg == "b" || arg == "black")
            return BLACK;
        if (arg == "w" || arg == "white")
            return WHITE;
        throw GtpFailure() << "invalid color: " << arg;
    }

    Color BlackWhiteArg(GtpCommand& cmd)
    {
        cmd.CheckNuArg(1);
        return BlackWhiteArg(cmd, 0);
    }

    KoRule KoRuleArg(GtpCommand& cmd, size_t number)
    {
        string arg = cmd.ArgToLower(number);
        if (arg == "simple")
            return SIMPLE_KO;
        else if (arg == "superko")
            return SUPERKO;
        else if (arg == "pos_superko")
            return POSITIONAL_SUPERKO;
        throw GtpFailure("ko rule must be: simple, superko, or pos_superko");
    }

    string GetKoRuleString(KoRule koRule)
    {
        switch (koRule)
        {
        case SIMPLE_KO:
            return "simple";
        case SUPERKO:
            return "superko";
        case POSITIONAL_SUPERKO:
            return "pos_superko";
        }
        assert(false);
        return "";
    }
}

//----------------------------------------------------------------------------

Engine::Engine()
{
    Register("all_legal", &Engine::CmdAllLegal);
    Register("amigo-group", &Engine::CmdGroup);
    Register("amigo-intboard", &Engine::CmdIntboard);
    Register("amigo-killable", &Engine::CmdKillable);
    Register("amigo-play_reason", &Engine::CmdPlayReason);
    Register("amigo-saveable", &Engine::CmdSaveable);
    Register("amigo-settings", &Engine::CmdSettings);
    Register("boardsize", &Engine::CmdBoardsize);
    Register("clear_board", &Engine::CmdClearBoard);
    Register("genmove", &Engine::CmdGenmove);
    Register("gogui-analyze_commands", &Engine::CmdAnalyzeCommands);
    Register("is_legal", &Engine::CmdIsLegal);
    Register("komi", &Engine::CmdKomi);
    Register("play", &Engine::CmdPlay);
    Register("reg_genmove", &Engine::CmdRegGenmove);
    Register("showboard", &Engine::CmdShowboard);
    Register("undo", &Engine::CmdUndo);
}

/** Return all legal moves.
    Compatible with the GNU Go command all_legal.
*/
void Engine::CmdAllLegal(GtpCommand& cmd)
{
    Color c = BlackWhiteArg(cmd);
    for (int x = 0; x < m_board.GetSize(); ++x)
        for (int y = 0; y < m_board.GetSize(); ++y)
        {
            Point p(x, y);
            if (m_board.IsLegal(p, c))
                cmd << p << ' ';
        }
}

/** Get analyze-command configuration for GoGui. */
void Engine::CmdAnalyzeCommands(GtpCommand& cmd)
{
    cmd.CheckArgNone();
    cmd <<
        "plist/All Legal/all_legal %c\n"
        "sboard/Claim/amigo-intboard claim %c\n"
        "sboard/Connect Map/amigo-intboard connect_map %c\n"
        "hpstring/Group/amigo-group %p\n"
        "sboard/Group IDs/amigo-intboard group_ids black\n"
        "varpo/Killable/amigo-killable %p\n"
        "sboard/Ndbord/amigo-intboard ndbord %c\n"
        "string/Play Reason/amigo-play_reason\n"
        "sboard/Protected/amigo-intboard prot_points %c\n"
        "varc/Reg Genmove/reg_genmove %c\n"
        "varp/Saveable/amigo-saveable %p\n"
        "string/Showboard/showboard\n"
        "param/Settings/amigo-settings\n"
        "sboard/SGroups/amigo-intboard sgroups black\n";
}

/** Change board size.
    Standard GTP command. */
void Engine::CmdBoardsize(GtpCommand& cmd)
{
    int size = cmd.Arg<int>();
    if (size < 9 || size > 19)
        throw GtpFailure() << "board size " << size << " not supported";
    m_board.Init(size);
}

/** Clear board.
    Standard GTP command. */
void Engine::CmdClearBoard(GtpCommand& cmd)
{
    cmd.CheckArgNone();
    m_board.Init();
}

/** Generate a move.
    Standard GTP command. */
void Engine::CmdGenmove(GtpCommand& cmd)
{
    Genmove(cmd, true);
}

/** Get group info.
    Arguments: point <br>
    Returns: Multi-line list of group properties. */
void Engine::CmdGroup(GtpCommand& cmd)
{
    Point p = StoneArg(cmd);
    cmd << Interface::GetGroupInfo(m_board, m_board.GetToPlay(), p);
}

/** Get internal data as integer board.
    Data types:
    - @c claim
    - @c group_ids
    - @c connect_map
    - @c ndbord
    - @c prot_points
    - @c sgroups
    Arguments: type color <br>
    Returns: The integer board for the given data type and color. The response
    is compatible with GoGui's analyze command type @c sboard. */
void Engine::CmdIntboard(GtpCommand& cmd)
{
    cmd.CheckNuArg(2);
    string id = cmd.Arg(0);
    Color c = BlackWhiteArg(cmd, 1);
    IntBoard board = Interface::GetIntBoard(m_board, c, id);
    cmd << '\n';
    for (int y = m_board.GetSize() - 1; y >= 0; --y)
    {
        for (int x = 0; x < m_board.GetSize(); ++x)
            cmd << setw(3) << board.Get(x, y) << ' ';
        cmd << '\n';
    }
}

/** Check if move is legal.
    Compatible with the GNU Go command is_legal. */
void Engine::CmdIsLegal(GtpCommand& cmd)
{
    cmd.CheckNuArg(2);
    Color c = BlackWhiteArg(cmd, 0);
    Point p = PointArg(cmd, 1);
    cmd << (m_board.IsLegal(p, c) ? "1" : "0");
}

/** Check if block of stones is killable.
    Arguments: point <br>
    Returns: move to kill block or nothing, if block is safe */
void Engine::CmdKillable(GtpCommand& cmd)
{
    Point move;
    if (Interface::IsKillable(m_board, StoneArg(cmd), move))
        cmd << move;
}

/** Set komi.
    Standard GTP command.
    The komi is not used by AmiGo, this command exists only for
    compatibility. */
void Engine::CmdKomi(GtpCommand& cmd)
{
    cmd.Arg<float>();
    // Ignore komi value, AmiGo does not make use of it
}

/** Get name of Go engine.
    Standard GTP command. */
void Engine::CmdName(GtpCommand& cmd)
{
    cmd.CheckArgNone();
    cmd << "AmiGo";
}

/** Play move.
    Standard GTP command. */
void Engine::CmdPlay(GtpCommand& cmd)
{
    cmd.CheckNuArg(2);
    Color c = BlackWhiteArg(cmd, 0);
    if (cmd.ArgToLower(1) == "pass")
        m_board.Play(c, Point());
    else
        m_board.Play(c, PointArg(cmd, 1));
}

/** Return play reason for last generated move.
    Returns a single word play reason for the last move generated by AmiGo. */
void Engine::CmdPlayReason(GtpCommand& cmd)
{
    cmd.CheckArgNone();
    cmd << Interface::GetPlayReason();
}

/** Check if block of stones is saveable.
    Arguments: point <br>
    Returns: move to save block or nothing, if block is dead */
void Engine::CmdSaveable(GtpCommand& cmd)
{
    Point move;
    if (Interface::IsSaveable(m_board, StoneArg(cmd), move))
        cmd << move;
}

/** Get current board state.
    Standard GTP command. */
void Engine::CmdShowboard(GtpCommand& cmd)
{
    cmd.CheckArgNone();
    cmd << '\n';
    m_board.Print(cmd);
}

/** Get AmiGoGtp version.
    Standard GTP command. <br>
    Returns: The version of AmigoGtp or en empty string if this executable
    was not compiled with the define VERSION. */
void Engine::CmdVersion(GtpCommand& cmd)
{
    cmd.CheckArgNone();
#ifdef VERSION
    cmd << VERSION;
#else
    cmd << "";
#endif
}

/** Generate a move without playing it.
    Standard GTP command. */
void Engine::CmdRegGenmove(GtpCommand& cmd)
{
    Genmove(cmd, false);
}

/** Set AmiGoGtp parameters.
    This command is compatible with the GoGui analyze command type @c param.
    The following parameters exist:
    - [bool] @c show_trees Show searches during move generation with GoGui
      Live Graphics commands
    - [bool] @c suicide_allowed Accept suicide moves in play command
    - @c level Set play level 0 to 7
    - @c ko_rule Set ko rule (@c simple, @c superko, @c pos_superko) */
void Engine::CmdSettings(GtpCommand& cmd)
{
    if (cmd.NuArg() == 0)
    {
        cmd << "[bool] show_trees " << Interface::GetShowTrees() << '\n'
            << "[bool] suicide_allowed " << m_board.GetSuicideAllowed()
            << '\n'
            << "[list/simple/superko/pos_superko] ko_rule "
            << GetKoRuleString(m_board.GetKoRule()) << '\n'
            << "[list/0/1/2/3/4/5/6/7] level " << Interface::GetPlayLevel()
            << '\n';
    }
    else if (cmd.NuArg() == 2)
    {
        string name = cmd.Arg(0);
        if (name == "show_trees")
            Interface::SetShowTrees(cmd.Arg<bool>(1));
        else if (name == "suicide_allowed")
            m_board.SetSuicideAllowed(cmd.Arg<bool>(1));
        else if (name == "ko_rule")
            m_board.SetKoRule(KoRuleArg(cmd, 1));
        else if (name == "level")
            Interface::SetPlayLevel(cmd.ArgMinMax<int>(1, 0, 7));
    }
    else
        throw GtpFailure("invalid number of arguments");
}

/** Undo last move.
    Standard GTP command. */
void Engine::CmdUndo(GtpCommand& cmd)
{
    cmd.CheckArgNone();
    if (! m_board.IsUndoPossible())
        throw GtpFailure("no undo possible");
    m_board.Undo();
}

void Engine::Genmove(GtpCommand& cmd, bool play)
{
    Color c = BlackWhiteArg(cmd);
    Point p = Interface::GenMove(m_board, c);
    if (play)
        Play(p, c);
    cmd << p;
}

void Engine::Play(const Point& p, Color c)
{
    if (! m_board.IsLegal(p, c))
        throw GtpFailure() << "Illegal move " << p;
    m_board.Play(c, p);
}

Point Engine::PointArg(GtpCommand& cmd, size_t number) const
{
    int boardSize = m_board.GetSize();
    int x, y;
    istringstream buffer(cmd.ArgToLower(number));
    char xChar;
    buffer >> xChar >> y;
    if (buffer)
    {
        if (xChar > 'i')
            --xChar;
        x = xChar - 'a';
        --y;
        if (x >= 0 && x < boardSize && y >= 0 && y < boardSize)
            return Point(x, y);
    }
    throw GtpFailure() << "invald point " << cmd.Arg(number);
}

void Engine::Register(const string& name, GtpCallback<Engine>::Method method)
{
    GtpEngine::Register(name, new GtpCallback<Engine>(this, method));
}

Point Engine::StoneArg(GtpCommand& cmd, size_t number) const
{
    Point p = PointArg(cmd, number);
    if (! m_board.IsOccupied(p))
        throw GtpFailure("point not occupied");
    return p;
}

Point Engine::StoneArg(GtpCommand& cmd) const
{
    cmd.CheckNuArg(1);
    return StoneArg(cmd, 0);
}

//----------------------------------------------------------------------------

} // namespace amigogtp
